Skip to content

feat(ai): add the ai resource with an OpenAI-compatible gateway#178

Closed
ItamarZand88 wants to merge 24 commits into
mainfrom
itamar/ai-gateway
Closed

feat(ai): add the ai resource with an OpenAI-compatible gateway#178
ItamarZand88 wants to merge 24 commits into
mainfrom
itamar/ai-gateway

Conversation

@ItamarZand88

@ItamarZand88 ItamarZand88 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an ai resource that gives a workload an OpenAI-compatible endpoint for model inference, in two modes. By default it's keyless: for the host cloud's own models — Bedrock on AWS, Vertex on GCP, Foundry on Azure — a loopback gateway signs each request with the workload's ambient cloud identity, so there are no model API keys to store. A workload can instead bring its own provider key (e.g. OpenAI), in which case the client talks to that provider directly with the vault-resolved key and the gateway isn't involved.

What happens on the keyless path, when a workload declares an ai binding for one of the host cloud's models:

  1. A loopback AI gateway starts next to the app and its URL is placed in the environment; the app points a normal OpenAI-compatible client at it.
  2. The gateway translates each OpenAI-style request into the target cloud's native model call — Bedrock InvokeModel, Vertex rawPredict, or the Foundry Anthropic endpoint — and signs it with the workload's ambient cloud credentials. ← the heart of the change
  3. The response, including streamed tokens, is translated back to the OpenAI/Anthropic shape and returned to the app.

For a cloud's own models, this changes AI access from managing model API keys to using the cloud account the workload already runs in.

What I did

  • Add a gateway engine (Rust) that translates OpenAI-shaped requests to each cloud's model API and signs them with ambient credentials.
  • Add the ai resource to the SDK, plus the controllers and per-cloud setup that grant a workload access to that cloud's models.
  • Support a bring-your-own-key mode: the same ai binding can point at an external provider with the workload's own key (vault-resolved, redacted in logs), which bypasses the gateway and calls the provider directly.
  • Scope the ambient model-invocation grant to inference only: no deployment or data-plane writes.
  • Ship the gateway as one standalone binary everywhere: containers run it as their entrypoint, the SDK spawns it on first use and reads back the URL it prints, and compiled single-file Workers embed the binary and extract and spawn it at runtime.
  • Make getAvailableModels report real availability: the gateway probes each catalog model with a one-token request under the workload's own inference credential and lists only what the cloud actually has enabled (a 429 counts as enabled but rate-limited; 400/401/403/404 drop the model). Each entry carries provider and displayName so an app can build a model picker, and the example README documents each model's one-time enablement step per cloud.
  • Type the client surface: chat.completions.create and responses.create return OpenAI's own result types (a types-only dev dependency), so callers read .choices[0].message.content with no cast.
  • Add an ai-quickstart-ts example built around "list the available models, then pick one".

Files touched

  • crates/alien-ai-gateway — the gateway engine (router, model catalog, availability probe, per-cloud translation + signing) and the alien-ai-gateway launcher binary.
  • packages/ai-gateway — the TypeScript wrapper: resolves and spawns the gateway binary, and the ambient vs BYO-key connection resolution.
  • crates/alien-core, packages/core, packages/sdk — the ai resource type, the model catalog with per-model activation metadata, and the SDK surface (plus embedding the gateway binary into compiled Workers via packages/package-layout and crates/alien-build).
  • crates/alien-infra — the AI controllers and runtime wiring.
  • crates/alien-terraform + crates/alien-cloudformation — the per-cloud setup emitters and their snapshots.
  • crates/alien-permissions/permission-sets/ai/* — the ai/invoke grant (plus heartbeat/management/provision).
  • examples/ai-quickstart-ts — a runnable example.

How I tested

  • Manually: From a deployed test worker, real Claude requests went through the gateway with no API keys configured — Bedrock (AWS) and the Foundry Anthropic endpoint (Azure) both returned completions, including streaming. I also ran the gateway on my machine against real Bedrock and hit /v1/models: 36 of the 42 AWS catalog models came back listed and enriched, and the 6 dropped were Claude ids that account/region cannot serve.
  • Compiled Worker: bun build --compile of a Worker that calls ai() — the embedded gateway binary is extracted and spawned from inside the single-file binary and serves a loopback URL (the package-layout compile smoke).
  • Unit tests: cargo nextest across the workspace (CI-fast filter), including the availability-filter suite (mock upstream: an enabled model is kept and enriched, a 403 model is dropped, a 500 keeps the model and re-probes on the next call, a second call is served from cache); SDK vitest + biome; terraform validate on the AI setup-emitter output and the CloudFormation snapshot suite; generated-schema drift check clean. The live Bedrock test reads the shared test env and runs in the credentialed job.
  • E2E (ran green on real Bedrock): the comprehensive TypeScript app lists every model getAvailableModels returns and invokes each one, failing if a listed model is not invocable. That run passed after it caught a real filter bug: Bedrock answers 400 "The provided model identifier is invalid" for a model the account cannot address, and I had been counting 400 as available, so it now drops the model (400 joins 401/403/404) and the list only ever contains invocable models.
  • Anything I couldn't test: Vertex (GCP) live inference is pending a model-garden entitlement on the test project — unrelated to this code; the GCP emitter path is covered by snapshots + terraform validate.

One non-AI fix rides along

The cloud e2e for this PR overflowed its test thread's stack during deploy. It is a pre-existing issue, independent of the AI code: the deployment reconcile polls a deep async chain (executor to controllers to the cloud SDKs' tower/hyper/rustls futures) that needs about 3MB in a debug build, over tokio's 2MB default thread stack. Release binaries fit 2MB and are unaffected. RUST_MIN_STACK=8MB for dev/test threads via .cargo/config.toml fixes it, matching the headroom alien-worker-runtime already gives its runtime. It rides along here because this PR's own e2e could not run without it.

I also ran a security review on the diff. What it checked:

  • Over-scoped model grant. ai/invoke grants inference only — AWS bedrock:InvokeModel* (+ mantle CreateInference scoped to project/default, not project/*); a GCP custom role of just aiplatform.endpoints.predict/explain (not roles/aiplatform.user, which carries deploy + dataset-export); Azure "Cognitive Services OpenAI User" (data-plane only). No deployment writes or data reads. crates/alien-permissions/permission-sets/ai/invoke.jsonc.
  • Availability probe surface. The /v1/models probe sends a one-token inference request signed with that same inference-only grant; it adds no control-plane permission, cannot enable or subscribe anything, and its logs carry only the model id and status, never request or response bodies. crates/alien-ai-gateway/src/availability.rs.
  • Credential exposure. On the keyless path the gateway signs with the workload's ambient cloud identity — no key is stored. On the BYO-key path the provider key is a vault-resolved secret: its Debug impl redacts it (crates/alien-core/src/bindings/ai.rs) and it's resolved into the worker environment, not logged or held in control-plane state. crates/alien-ai-gateway/src/config.rs.
  • Off-host reachability. The gateway binds the loopback interface and hands the app its URL out of band, so it isn't reachable from other hosts. crates/alien-ai-gateway/src/lib.rs.
  • BYO-key providers. An external (bring-your-own-key) provider is explicitly not served by the ambient-credential gateway path. crates/alien-ai-gateway/src/config.rs.

Nothing turned up.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Too many files changed for review. (176 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

Comment thread examples/ai-quickstart-ts/alien.ts Outdated
@@ -0,0 +1,26 @@
import * as alien from "@alienplatform/core"

// A model-less AI resource. The customer's cloud serves the inference; the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"model-less AI resource" - confusing
'embedded gateway workload ambient identity etc -- implementation details remove from example

Comment thread examples/ai-quickstart-ts/alien.ts Outdated
const api = new alien.Worker("api")
.code({ type: "source", src: "./", toolchain: { type: "typescript" } })
.publicEndpoint("api")
// Linking injects ALIEN_LLM_BINDING and exposes the gateway as ALIEN_AI_GATEWAY_URL.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this comment to be here

Comment thread examples/ai-quickstart-ts/src/index.ts Outdated
if (!question) {
return c.json({ error: "pass a question as ?q=..." }, 400)
}
const llm = ai("llm")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai("llm") is very confusing :) lets think of better name than "llm"

Comment thread examples/ai-quickstart-ts/src/index.ts Outdated
const completion = (await llm.chat.completions.create({
model,
messages: [{ role: "user", content: question }],
})) as { choices?: Array<{ message?: { content?: string } }> }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is a bit messy.. why is this necessary? need a much cleaner API

@@ -0,0 +1,178 @@
//! Embedded, protocol-agnostic AI gateway: a loopback HTTP server that injects the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this crate to alien-ai-gateway

@@ -0,0 +1,148 @@
//! Live verification against real AWS Bedrock. Ignored by default — it makes a
//! real inference call and needs ambient AWS credentials in the environment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not ignore these tests by default, they're very important (same for foundry vertex etc). And lets run them and make sure they're all passing.

You can load our alien-test-target account credentials from ../env.test - see this pattern in the alien-bindings tests.

//! real inference call and needs ambient AWS credentials in the environment.
//!
//! Run it with:
//! eval "$(aws configure export-credentials --profile <p> --format env)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this thing once we'll load ../env.test

Comment thread crates/alien-ai-gateway-node/src/lib.rs Outdated
@@ -0,0 +1,52 @@
//! Node-API addon for the alien AI gateway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm.. let's think if we really need this crate. Maybe we can just start an ./alien-ai-gateway process from the typescript package?

discuss a little bit with claude what are the advantages / disadvantages of this

Comment thread packages/ai-gateway/PACKAGE_LAYOUT.md Outdated
@@ -0,0 +1,77 @@
# `@alienplatform/ai-gateway` — package layout contract

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is all this? needed?

@@ -0,0 +1,57 @@
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's think about it for a second. Do we really need a separate package for this? or should this just be inside @alienplatform/bindings?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked at folding this into @alienplatform/bindings and decided to keep it separate.

Main reason: bindings is in every worker (kv/storage/queue/vault), so if the gateway lives there too, every worker ships the alien-ai-gateway binary even when it never calls ai(). Keeping it on its own means only AI workers carry it.

They're also different beasts now. bindings is a napi addon loaded in-process, the gateway is a standalone binary we spawn. Different build and staging (it's why alien-build has the Napi/Binary split). One package for both felt off.

Re binary size: same conclusion. There's only one napi addon left now (bindings), and the gateway binary only gets embedded into compiled workers that actually use ai(), so a normal worker doesn't grow. Merging is what would bloat everything.

Left it separate for now, lmk if you disagree.

// module for each. They're real runtime dependencies, so the package stays
// self-contained.
external: [
"@alienplatform/bindings",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need both @alienplatform/bindings and @alienplatform/ai-gateway and not put them on the same thing? is there very large impact on the binary size?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept them separate (more on the package.json thread). The size question actually argues for it: the gateway binary only lands in compiled workers that call ai(), so merging into bindings would put it in every worker instead. No size win from combining.

ItamarZand88 added a commit that referenced this pull request Jul 23, 2026
Apply Alon's #178 review:
- Replace the alien-ai-gateway-node napi addon with spawning the standalone
  alien-ai-gateway binary. The SDK uses ALIEN_AI_GATEWAY_URL when a container
  launcher set it, else spawns the binary and reads the URL it prints; compiled
  Workers embed the binary and extract+spawn it at runtime. Deletes the addon
  crate + npm prebuilds; alien-build stages the binary (AddonKind::Binary).
- Type chat.completions/responses.create via OpenAI's result types (types-only
  devDependency), dropping the caller-side casts.
- Rename crate alien-gateway -> alien-ai-gateway.
- Trim the ai-quickstart example; rename the binding llm -> assistant.
- Live tests load .env.test (Bedrock un-ignored; Foundry/Vertex/Bedrock-Claude
  gated pending minted tokens + model grants); exclude the live binaries from
  the fast CI job.

@lilienblum lilienblum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a deeper pass on the non-example parts. Two high-severity issues (a BYO provider-validation gap, and a process-lifecycle bug that hangs one-shot callers) plus two cheap fixes (a 2 MB proxy body limit and a Frozen-vs-Live GCP gap). The gateway proxy core itself looks solid.

Comment thread packages/ai-gateway/src/client.ts Outdated
function providerBaseUrl(provider: string): string {
const override = process.env.ALIEN_AI_LOCAL_BASE_URL
if (override) return override.replace(/\/$/, "")
return provider === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com"

@lilienblum lilienblum Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High. An unvalidated provider can silently send a BYO key to the wrong host.

provider is a free-form string at every layer (ExternalAiBinding.provider: String, binding.ts z.string(), no allowlist), and providerBaseUrl here falls back to https://api.openai.com for anything that isn't exactly "anthropic". _postSurface then attaches the BYO key as Authorization: Bearer to that base. So a binding with provider: "Anthropic" (casing), "google", "groq", or a typo, and no ALIEN_AI_LOCAL_BASE_URL override, ships the customer's key to OpenAI. Only the openai BYO path is tested, so these paths are unexercised.

Suggested fix. Validate provider against a known set at the resolve boundary and fail closed instead of defaulting to OpenAI.

(Correcting my earlier note: the "anthropic" branch itself is fine. https://api.anthropic.com/v1/chat/completions with Bearer is Anthropic's OpenAI-compat endpoint. The one real gap on that path is responses.create, since the compat layer doesn't serve /v1/responses, so a BYO-anthropic Responses call would 404 while chat/completions works.)

child.on("exit", onGone)
child.stdout?.resume()
child.stderr?.resume()
child.unref()

@lilienblum lilienblum Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High (scoped to one-shot processes). child.unref() here does not let a short-lived host process exit.

unref() drops only the process handle, but the resume() calls just above put stdout/stderr into flowing mode, which keeps those pipe handles referenced. I reproduced this with a detached long-running child: after resume() + child.unref(), the parent was still alive until the child exited. Long-running Workers want to stay up anyway, but a CLI, batch job, or test process that calls ai() can hang forever.

Suggested fix. Unref the stdout/stderr pipe handles after draining them, or restructure stdio so no referenced pipes remain after the readiness handshake. Note that ChildProcess.stdout/stderr are typed as Readable, so direct child.stdout?.unref() does not type-check even though the runtime pipe is a net.Socket; use a type-safe socket/narrowing helper. Add a one-shot subprocess test that starts the gateway and asserts the host exits.

client: reqwest::Client::new(),
models_cache,
});
Router::new()

@lilienblum lilienblum Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Should-fix (easy). Not merge-blocking, but it silently rejects valid large requests.

The handlers extract body: Bytes and the router never calls DefaultBodyLimit::disable(), so axum 0.8's default 2 MB limit 413s requests before they reach the upstream. That's reachable for vision/base64 images and long tool-heavy conversations.

Suggested fix. Since this is a pure proxy, disable the limit and let the upstream enforce its own: Router::new().layer(DefaultBodyLimit::disable()).

//! Vertex AI endpoint without a cloud round-trip.
//!
//! The `aiplatform.googleapis.com` API enablement is handled by the
//! `GcpServiceActivationEmitter` when the preflight injects a

@lilienblum lilienblum Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Should-fix (easy). Breaks the Frozen/Terraform GCP path only (the native controller is fine).

The doc here says aiplatform enablement is handled by GcpServiceActivationEmitter via an injected ServiceActivation, but get_required_services() in gcp_service_activation.rs has no "ai" arm, so nothing injects it. terraform apply still succeeds (the emitter just produces no google_project_service); the failure surfaces later at gateway runtime as SERVICE_DISABLED when Vertex is first called on a project that didn't already have the API on. The native controller enables it at runtime (ai/gcp.rs), so only Terraform-mode GCP deploys are affected.

Suggested fix. Add an "ai" arm to get_required_services().

// A small curated chat-model list for the BYO-key picker (we don't proxy the provider's own
// /v1/models, which returns hundreds of non-chat entries).
function defaultModels(provider: string): string[] {
return provider === "anthropic"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High (BYO Anthropic). getAvailableModels() returns only two retired model families for this provider.

claude-3-5-sonnet-latest and claude-3-5-haiku-latest point at Claude 3.5 families whose pinned models were retired on 2025-10-28 and 2026-02-19 respectively. Anthropic documents that requests to retired models fail: https://platform.claude.com/docs/en/about-claude/model-deprecations. Because staticModels bypasses the provider models endpoint, a BYO-Anthropic picker receives only unusable IDs and the first real completion fails.

Suggested fix. Return active IDs such as claude-sonnet-5 / claude-haiku-4-5 and cover the Anthropic branch with a request test. Better, query the provider models endpoint so this list does not silently age out again.

Comment thread crates/alien-ai-gateway/src/creds.rs Outdated
/// Cache-then-fetch the workload's projected-identity token from the instance metadata
/// service. `source` must be `Gcp` or `Azure`.
async fn metadata_token(&self, source: &BearerSource) -> Result<String> {
if let Some((tok, exp)) = self.cache.lock().await.as_ref() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High on projected-identity GCP/Azure workloads. This cache miss is not single-flight.

The mutex protects only the lookup and is released before the metadata request. The first /v1/models call uses join_all to launch every model probe concurrently (14 on GCP, 12 on Azure); each probe authorizes independently, so they can all observe an empty cache and hit IMDS at once. The same stampede happens when a token expires under concurrent inference. If IMDS throttles, model probes become indeterminate, the list is deliberately left uncached, and the next call repeats the fan-out.

Suggested fix. Serialize refreshes behind the cache lock or store a shared in-flight refresh future/OnceCell, then add a concurrent cold-cache test that asserts exactly one metadata request.

@@ -0,0 +1,2137 @@
//! The pure proxy: route a request to the model's native cloud endpoint, inject the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Structural. This new module lands at 2,137 lines and has too many reasons to change.

The first ~974 lines combine shared routing/transport, cloud URL selection, three provider adapters, Bedrock request normalization, and an event-stream decoder; another ~1,160 lines of tests live in the same file. availability.rs already imports several of these internals, so the boundary is starting coupled rather than cohesive.

Suggested fix. Decompose before this becomes the permanent extension point: keep dispatch/common transport in router/mod.rs, move Bedrock, Vertex, and Foundry handling into focused modules, and isolate the Bedrock event-stream codec with its tests. That leaves future provider changes local and keeps each module reviewable.

Introduce built-in AI: an `ai` resource that provisions keyless,
in-account model access through an embedded gateway. Adds the Rust
gateway engine and napi addon, the TypeScript SDK wrapper, the per-cloud
controllers and setup emitters (AWS Bedrock, GCP Vertex, Azure Foundry),
the ai/invoke permission sets, and quickstart examples.
- The napi error envelope now carries httpStatusCode and hint, so a gateway
  startup failure (e.g. BindingConfigInvalid at http_status_code 400) is no
  longer flattened to a 500 when it crosses into JS.
- parseSse preserves the malformed-chunk cause as a structured error source
  instead of folding it into the message string.
- Name the condition the anthropic-beta merge test enforces rather than the
  old bug it guards against.
The ai() client resolved its napi addon by filesystem lookup, which cannot
work inside a `bun build --compile` single-file binary — so a compiled Worker
calling ai() failed while kv/storage/queue/vault worked. Mirror the bindings
embedded-addon mechanism end to end:

- The ai-gateway loader gains an embedded slot (registerEmbeddedAddon); loadAddon
  prefers it over the filesystem/prebuild resolution that can't run in a binary.
- ai-gateway/native installEmbeddedAddon() registers the bun-embedded addon.
- @alienplatform/sdk/native installs both the bindings and ai-gateway addons; the
  SDK keeps both external so the compiled binary shares one module for each.
- alien-build stages the ai-gateway addon alongside bindings, best-effort: a
  Worker that resolves ai-gateway transitively through the SDK but never calls
  ai() still builds (a missing addon for the target is skipped, not an error).

Verified by the package-layout compile-smoke (a compiled binary resolves the
SDK's ai + storage after both staged .node files are removed) and a loader unit
test that loadAddon prefers the embedded addon.
The Postgres runtime binding (packages/bindings/src/postgres.ts,
getPostgresConnection, cloud secret-manager deps) and the AI+Postgres
chatbot example rode in from the shared source branch. main already
ships the Postgres resource and dials the DB directly, and
PACKAGE_LAYOUT.md keeps getPostgresConnection off the SDK root, so this
copy is superseded. Remove it so this PR stays AI-only.
Apply Alon's #178 review:
- Replace the alien-ai-gateway-node napi addon with spawning the standalone
  alien-ai-gateway binary. The SDK uses ALIEN_AI_GATEWAY_URL when a container
  launcher set it, else spawns the binary and reads the URL it prints; compiled
  Workers embed the binary and extract+spawn it at runtime. Deletes the addon
  crate + npm prebuilds; alien-build stages the binary (AddonKind::Binary).
- Type chat.completions/responses.create via OpenAI's result types (types-only
  devDependency), dropping the caller-side casts.
- Rename crate alien-gateway -> alien-ai-gateway.
- Trim the ai-quickstart example; rename the binding llm -> assistant.
- Live tests load .env.test (Bedrock un-ignored; Foundry/Vertex/Bedrock-Claude
  gated pending minted tokens + model grants); exclude the live binaries from
  the fast CI job.
getAvailableModels now returns only the models the deployment's cloud has
actually enabled, probed at runtime, instead of the static catalog superset.
The gateway sends a tiny max_tokens:1 request per catalog model on the first
/v1/models (cached per process), signed with the workload's ambient credential,
and keeps a model only if it authenticated (2xx/429/400); 401/403/404 drop it.
No permission change: the probe rides the existing ai/invoke inference grant.

Enriches AiModel with provider + displayName for a picker, adds a static
activation field documenting each model's one-time enablement step, iterates
the e2e over every listed model, and updates the example to discover-then-pick.
…uilds

The release pipeline built the deleted alien-ai-gateway-node napi crate. It
now builds the alien-ai-gateway binary in the addon matrix (darwin legs on the
addon's targets; linux legs statically against musl, so one binary per arch
serves both glibc and Alpine), generates the six per-triple prebuild manifests
at publish time instead of keeping them checked in, and publishes them before
the wrapper with exact-version optionalDependencies. The dry-run path covers
the whole build and publish flow, matching publish-bindings.
…ration test

/v1/models now probes every catalog model against the mock upstream, and an
unmatched probe 404s and drops the model, so the Claude catalog assertion needs
the InvokeModel path answered.
The deployment reconcile polls a deep async chain on a single stack: the
executor drives each resource controller, which drives the cloud SDKs'
tower/hyper/rustls futures. In debug builds those frames are un-inlined and the
chain needs about 3MB, over tokio's 2MB default thread stack, so the alien-test
cloud e2e (push_/pull_) overflows its libtest test thread and aborts during
initial setup.

Release binaries are unaffected (inlined frames fit the 2MB default, confirmed
by running the same deploy in release), so this only sizes dev/test threads via
a cargo [env] entry. 8MB matches the headroom alien-worker-runtime already gives
its runtime for the same deep-async reason.
The availability probe counted 400 as "enabled", assuming it proved the endpoint
authed and only rejected the minimal probe body. The cloud e2e disproved that:
Bedrock answers "The provided model identifier is invalid" with 400 for a model
the account cannot address, so qwen3-coder-next and claude-mythos-5 were listed
by getAvailableModels and then failed on the first real call.

The probe body is minimal and well formed, so a 400 is about the model rather
than the request. Classifying it as unavailable keeps the contract the e2e
asserts: every listed model is invocable. A model that only ever answers 400
now drops out of the list instead of being advertised and breaking at runtime.
Draining the child's stdio with resume() keeps the pipe sockets referenced, so a process that calls ai() once and returns never exits. Unref both stdio sockets alongside the child.
The native controller enables aiplatform.googleapis.com at runtime, but a Frozen/Terraform GCP deploy relies on this activation being injected here, or the first Vertex call fails with SERVICE_DISABLED.
Hold the metadata cache lock across the IMDS fetch so a burst of concurrent probes (the /v1/models fan-out authorizes every model at once) collapses to one refresh instead of stampeding the metadata service.
providerBaseUrl defaulted any provider other than "anthropic" to OpenAI's endpoint, and the projected BYO key is attached as a bearer token there, so a typo or unlisted provider shipped the customer's key to OpenAI. Resolve only the providers we have a base URL for and throw otherwise; the ALIEN_AI_LOCAL_BASE_URL override still allows any OpenAI-compatible provider.
The BYO-Anthropic model picker returned retired claude-3-5-* ids. Return the current-generation aliases api.anthropic.com serves instead.
router.rs had grown to hold the transport, per-cloud URL dispatch, three provider adapters, the Bedrock body normalization, and the event-stream codec in one file. Move each provider's request path into its own module under router/ (bedrock/vertex/foundry) plus the event-stream decoder (eventstream), keeping mod.rs as the transport and dispatch. Behavior-preserving; the shared integration test harness stays in mod.rs.
The gateway is a pure proxy, so the upstream enforces its own body size, but axum's 2 MB default rejected large legitimate requests (base64 vision images, long tool-heavy conversations) with 413 before they ever left us. Disable the limit on the router.
reqwest has no default request timeout, so a hung instance metadata service would wedge /v1/models forever — and holding the single-flight lock across the fetch makes that block every waiting caller, not just one. Cap the fetch at 10s (the endpoint is link-local and answers in well under a second) so a real hang fails fast instead of hanging.
Anthropic's OpenAI-compatible host serves /v1/chat/completions but not the Responses API, so a BYO-Anthropic responses.create() returned an opaque 404. Detect the Anthropic native base and throw a clear reason before the request; an ALIEN_AI_LOCAL_BASE_URL override resolves elsewhere and is not blocked.
The three new AI ImportData types were never registered in the snapshot map, so the pinned schemas had no AI entry and the test passed without covering them.
Claude over classic InvokeModel signs with bedrock, not bedrock-mantle; bedrock-mantle serves the Responses API. The doc had the two the other way round.
@ItamarZand88
ItamarZand88 deleted the itamar/ai-gateway branch July 24, 2026 22:46
@ItamarZand88
ItamarZand88 restored the itamar/ai-gateway branch July 24, 2026 22:52
@ItamarZand88 ItamarZand88 reopened this Jul 24, 2026
@ItamarZand88
ItamarZand88 deleted the itamar/ai-gateway branch July 24, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants